/*
 * Copyright 2002-2005 the original author or authors.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package springfixture;

import java.util.HashMap;
import java.util.Map;

import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.util.ObjectUtils;

/**
 * This class manages the Spring ApplicationContext. It makes use of the Spring
 * JUnit classes because they implement support for caching of
 * ApplicationContexts and such. It is configured by the SpringConfig class.
 * 
 * @see springfixture.SpringConfig
 */
public class ApplicationContextHolder {

    /** The only instance. This class implements the Singleton pattern. */
    private static ApplicationContextHolder applicationContextHolder;

    /** Location of the configuration files. */
    private String[] configLocations;

    /**
     * Map of context keys returned by subclasses of this class, to Spring
     * contexts. This needs to be static, as JUnit tests are destroyed and
     * recreated between running individual test methods.
     */
    private static Map contextKeyToContextMap = new HashMap();

    /**
     * Mark the context with the given key as dirty. This will cause the cached
     * context to be reloaded before the next test case is executed.
     * <p>
     * Call this method only if you change the state of a singleton bean,
     * potentially affecting future tests.
     */
    protected final void setDirty(String contextKey[]) {
        String keyString = contextKeyString(contextKey);
        ConfigurableApplicationContext ctx = (ConfigurableApplicationContext) contextKeyToContextMap
                .remove(keyString);
        if (ctx != null) {
            ctx.close();
        }
    }

    /**
     * Subclasses can override this to return a String representation of their
     * context key for use in logging.
     * 
     * @param contextKey
     *            the context key
     */
    protected String contextKeyString(Object contextKey) {
        return ObjectUtils.nullSafeToString(contextKey);
    }

    /**
     * Obtain an ApplicationContext for the given key, potentially cached.
     * 
     * @param key
     *            the context key
     * @return the corresponding ApplicationContext instance (potentially
     *         cached)
     */
    protected final ConfigurableApplicationContext getContext(String[] key) {
        String keyString = contextKeyString(key);
        ConfigurableApplicationContext ctx = (ConfigurableApplicationContext) contextKeyToContextMap
                .get(keyString);
        if (ctx == null) {
            ctx = loadContext(key);
            ctx.registerShutdownHook();
            contextKeyToContextMap.put(keyString, ctx);
        }
        return ctx;
    }

    /**
     * Load an ApplicationContext from the given config locations.
     * 
     * @param locations
     *            the config locations
     * @return the corresponding ApplicationContext instance (potentially
     *         cached)
     */
    protected ConfigurableApplicationContext loadContext(String[] locations) {
        return new ClassPathXmlApplicationContext(locations);
    }

    /**
     * Get the current instance. (Singleton)
     * 
     * @return the only instance.
     */
    public static ApplicationContextHolder getApplicationContextHolder() {
        if (applicationContextHolder == null) {
            applicationContextHolder = new ApplicationContextHolder();
        }
        return applicationContextHolder;
    }

    /**
     * Get the ApplicationContext for the defined configLocation
     * 
     * @return The ApplicationContext for the previously defined configLocation
     */
    public ConfigurableApplicationContext getApplicationContext() {
        return applicationContextHolder.getContext(configLocations);
    }

    /**
     * Set the ConfigLocations. These are taken into account for the Spring
     * configuration. They are relative to the classpath. Using the file: URL
     * also other locations in the filesystem can be used.
     */
    public void setConfigLocation(String[] configLocation) {
        this.configLocations = configLocation;
    }

}